<?php
namespace Swork\Db;

use Swork\Bean\Holder\InstanceHolder;
use Swork\Client\ElasticSearch;
use Swork\Exception\ElasticSearchException;

class ElasticSearchModel extends ElasticSearch
{
    /**
     * 表名
     * @var string
     */
    private $tbl;

    /**
     * 字段结构（声明默认值）
     * @var array
     */
    private $cols;

    /**
     * 当前实例化对象
     * @var ElasticSearchModel
     */
    private static $instance = null;

    /**
     * 支持的数据类型
     */
    const TYPES = [
        'i' => 'integer',
        's' => 'text',
        'k' => 'keyword',
    ];

    /**
     * 初始表结构
     * @param string $tbl Index （相当于表名）
     * @param array $cols 字段结构（用于声明默认值）
     * @throws
     */
    public function __construct(string $tbl, array $cols)
    {
        $this->tbl = $tbl;
        $this->cols = $cols;
        parent::__construct();
    }

    /**
     * 单件实列化
     * @return ElasticSearchModel
     */
    public static function M()
    {
        $class = static::class;
        if (empty(self::$instance[$class]))
        {
            self::$instance[$class] = InstanceHolder::getClass($class);
        }
        return self::$instance[$class];
    }

    /**
     * 获取 Type （相当于表名）
     * @return string
     */
    public function getTbl()
    {
        return $this->tbl;
    }

    /**
     * 获取字段列表
     * @return array
     */
    public function getCols()
    {
        return $this->cols;
    }

    /**
     * 初始货储存库
     * @return bool
     * @throws
     */
    public function init()
    {
        //组装字段
        $cols = [];
        foreach ($this->cols as $name => $item)
        {
            $cols[$name]['type'] = self::TYPES[$item[0]] ?? 'text';
            if (isset($item[1]) && $item[1] != '')
            {
                $cols[$name]['analyzer'] = $item[1];
            }
            if (!isset($item[2]) && isset($item[1]) && $item[1] != '')
            {
                $cols[$name]['search_analyzer'] = $item[1];
            }
            if (isset($item[2]) && $item[2] != '')
            {
                $cols[$name]['search_analyzer'] = $item[2];
            }
        }

        //组装参数
        $result = $this->indexInit($this->tbl, $cols);
        if (!isset($result['acknowledged']) || $result['acknowledged'] != true)
        {
            return false;
        }

        //返回结果
        return true;
    }

    /**
     * 通过条件获取最高分一行数据
     * @param array $where 数据条件 ['id' => 123, 'name' => 'xixi', '$or' => [ id=>5, age => 9]]
     * @param string $cols 输出的字段，使用逗号分隔
     * @return bool|array
     * @throws
     */
    public function getRow(array $where = [], string $cols = '*')
    {
        //分析条件
        $query = new ElasticSearchQuery($this->tbl, $this->cols, $where);
        $condition = $query->getCondition();

        //获取数据
        $result = $this->search($this->tbl, $condition, $cols, [], 1);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //提取表结构字段
        $fields = $this->cols;
        foreach ($fields as $field => $item)
        {
            $fields[$field] = $field;
        }

        //提取结果
        $hits = $result['hits']['hits'][0] ?? [];
        if (count($hits) > 0)
        {
            $row = $hits['_source'];
            $cols = $cols == '*' ? array_keys($hits) : explode(',', $cols);

            //补充字段值
            unset($hits['_source']);
            foreach ($cols as $col)
            {
                if (isset($hits[$col]))
                {
                    $row[$col] = $hits[$col];
                }
            }
            return $row;
        }
        return false;
    }

    /**
     * 获取列表数据
     *
     * ['' => '深圳世界之窗'] 全表字段搜索
     *
     * ['col1' => '深圳世界之窗'] 生成
     *      query : {
     *          match : {'col1': '深圳世界之窗'}
     *      }
     *
     *
     * ['col1' => ['term' => '深圳世界之窗']] 生成
     *      query : {
     *          term : {'col1': '深圳世界之窗'}
     *      }
     *
     *
     * ['age' => ['>' => 20, '<' => 40]] 生成
     *      query : {
     *          rang : {'age': { "gte" : 20, "lte" : 40}}
     *      }
     *
     *
     * ['col1' => '深圳世界之窗', 'col2' => 'xxxx'] 生成
     *      query : {
     *          bool : {
     *              must : [
     *                  match : {'col1': '深圳世界之窗'},
     *                  match : {'col2': 'xxxx'}
     *              ]
     *          }
     *      }
     *
     *
     * ['col1' => '深圳世界之窗', 'age' => ['>' => 20, '<' => 40]] 生成
     *      query : {
     *          bool : {
     *              must : [
     *                  match : {'col1': '深圳世界之窗'},
     *                  range : {'age': { "gte" : 20, "lte" : 40}}
     *              ]
     *          }
     *      }
     *
     * ['$min_score' => 0.5] 表示最小匹配分值的结果
     *
     * @param array $where 数据条件 ['id' => 123, 'name' => 'xixi', '$or' => [ id=>5, age => 9]]
     * @param string $cols 输出的字段，使用逗号分隔(_id表示内部数据ID，_score表示匹配分数)
     * @param int $size 输出数量，0表示全部
     * @param int $idx 页码位置，从1开始，0表示不使用翻页
     * @param int $count 合计搜索查询的数量
     * @return array
     * @throws
     */
    public function getList(array $where = [], string $cols = '*', $size = 0, int $idx = 0, int &$count = 0)
    {
        //分析条件
        $query = new ElasticSearchQuery($this->tbl, $this->cols, $where);
        $condition = $query->getCondition();

        //获取数据
        $result = $this->search($this->tbl, $condition, $cols, [], $size, $idx);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //检查是否输出_id
        $isId = false;
        $isScore = false;
        if ($cols == '*' || $cols == '')
        {
            $isId = true;
            $isScore = true;
        }
        else
        {
            if (strpos($cols, '_id') !== false)
            {
                $isId = true;
            }
            if (strpos($cols, '_score') !== false)
            {
                $isScore = true;
            }
        }

        //提取结果
        $list = [];
        foreach ($result['hits']['hits'] as $hits)
        {
            if ($isId == true)
            {
                $hits['_source']['_id'] = $hits['_id'];
            }
            if ($isScore == true)
            {
                $hits['_source']['_score'] = $hits['_score'];
            }
            $list[] = $hits['_source'];
        }

        //提取符合要求的数量
        $count = $result['hits']['total']['value'];

        //返回结果
        return $list;
    }

    /**
     * 插入一条数据（等同创建索引）
     * @param array $data 需要插入的数据 {xxx:vvv, yy:vv}
     * @return array|bool
     * @throws
     */
    function insert(array $data)
    {
        //组装数据
        $values = [];
        foreach ($this->cols as $col => $val)
        {
            if (isset($data[$col]))
            {
                $values[$col] = $data[$col];
            }
        }

        //检查是否有数据
        if (count($values) == 0)
        {
            return false;
        }

        //提交数据
        $result = $this->documentInsert($this->tbl, $values);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //返回主键
        return $result['_id'];
    }

    /**
     * 批量插入数据
     * @param array $data 批量数据（二维数组）[{xxx:vvv, yy:vv},{}]
     * @return bool
     * @throws
     */
    function inserts(array $data)
    {
        //组装数据
        $datas = [];
        foreach ($data as $key => $item)
        {
            $values = [];
            foreach ($this->cols as $col => $val)
            {
                if (isset($item[$col]))
                {
                    $values[$col] = $item[$col];
                }
            }
            if (count($values) == 0)
            {
                continue;
            }
            $datas[] = $values;
        }

        //检查是否有数据
        if (count($datas) == 0)
        {
            return false;
        }

        //组装批量配置参数
        $params = [];
        foreach ($datas as $values)
        {
            $params[] = '{"index":{"_type":"_doc"}}';
            $params[] = json_encode($values, JSON_UNESCAPED_UNICODE);
        }
        $params[] = '';

        //提交数据
        $result = $this->documentBulk($this->tbl, $params);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //返回主键
        return $result['errors'] == false;
    }

    /**
     * 通过条件更新数据，建议优先使用 term 关键词来匹配
     * @param array $where 数据条件 ['id' => 123, 'name' => ['term' => 'xixi']]
     * @param array $data 需要更新的数据 {xxx:vvv, yy:vv}
     * @return bool
     * @throws
     */
    function update(array $where = [], array $data = [])
    {
        //分析条件
        $query = new ElasticSearchQuery($this->tbl, $this->cols, $where);
        $condition = $query->getCondition();

        //组装数据
        $params = [];
        foreach ($this->cols as $col => $val)
        {
            if (isset($data[$col]))
            {
                $va = $data[$col];
                $va = preg_replace('/\'/', '\\\'', $va);
                $params[$col] = "ctx._source.$col='$va'";
            }
        }

        //更新脚本
        $script = [
            'source' => join(';', $params),
            'lang' => 'painless'
        ];

        //提交数据
        $result = $this->documentUpdate($this->tbl, $condition, $script);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //返回影响行数
        return $result['updated'];
    }

    /**
     * 通过主键值更新数据
     * @param string $id 主键值
     * @param array $data 需要更新的数据 {xxx:vvv, yy:vv}
     * @return bool
     * @throws
     */
    function updateById(string $id, array $data)
    {
        //组装数据
        $params = [];
        foreach ($this->cols as $col => $val)
        {
            if (isset($data[$col]))
            {
                $va = $data[$col];
                $va = preg_replace('/\'/', '\\\'', $va);
                $params[$col] = "ctx._source.$col='$va'";
            }
        }

        //更新脚本
        $script = [
            'source' => join(';', $params),
            'lang' => 'painless'
        ];

        //提交数据
        $result = $this->documentUpdateById($this->tbl, $id, $script);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //返回影响行数
        return ($result['result'] == 'updated');
    }

    /**
     * 通过主键值删除数据
     * @param mixed $id 主键值
     * @return bool
     * @throws
     */
    function deleteById($id)
    {
        //提交数据
        $result = $this->documentDeleteById($this->tbl, $id);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //返回影响行数
        return ($result['result'] == 'deleted');
    }

    /**
     * 通过主键值获取数据
     * @param mixed $id 主键值
     * @param string $cols 输出的字段，使用逗号分隔
     * @return array
     * @throws
     */
    function getRowById($id, string $cols = '*')
    {
        //提交数据
        $result = $this->documentFetchById($this->tbl, $id, $cols);

        //检查是否有错误
        $this->checkDoesHaveError($result);
        if ($result['found'] != true)
        {
            return [];
        }

        //返回
        return $result['_source'];
    }

    /**
     * 检查是否存在主键值数据
     * @param mixed $id 主键值
     * @return array
     * @throws
     */
    function existById($id)
    {
        //获取第一个字段
        $cols = array_keys($this->cols)[0];

        //提交数据
        $result = $this->documentFetchById($this->tbl, $id, $cols);

        //检查是否有错误
        $this->checkDoesHaveError($result);

        //返回
        return $result['found'];
    }

    /**
     * 检查是否有异常
     * @param $result
     * @throws ElasticSearchException
     */
    private function checkDoesHaveError($result)
    {
        if (isset($result['error']))
        {
            throw new ElasticSearchException($result['error']['reason'], $result['status']);
        }
    }
}
